#include <alloc.h>
#include <dos.h>
#include <mem.h>
#include <stdio.h>
#include <string.h>
#include "COMMON.H"

#define TIMER_VECT 0x08 /* 8259 interrupt 0 */
#define TIMER_FREQ 1193 /* 1kHz, near enough */
#define OPL3_PORT 0x388

volatile unsigned ms, opl3_next;
unsigned ticks;

char far *song_data, far *song_ptr;
unsigned long song_len;
char cmd_shortdelay;
char cmd_longdelay;
char reg_table[128];
bool music_playing;

static void OPL3Frame(void);
static void OPL3Silence(void);

void far interrupt (*oldHandler)(void);

void far interrupt TimerHandler(void)
{
	unsigned short oldTicks;
	ms++;

	if (opl3_next)
	{
		opl3_next--;
		if (!opl3_next)
		{
			OPL3Frame();
		}
	}

	oldTicks = ticks;
	ticks += TIMER_FREQ;
	if (ticks < oldTicks) /* default handler runs every 65536 ticks */
	{
		oldHandler();
	} else {
		outportb(0x20, 0x20); /* acknowledge interrupt */
	}
}

void InitOPL3(void)
{
	/* install our own 1kHz timer handler */
	ticks = ms = opl3_next = 0;
	oldHandler = getvect(TIMER_VECT);
	setvect(TIMER_VECT, TimerHandler);
	outportb(0x43, 0x34);
	outportb(0x40, LOBYTE(TIMER_FREQ));
	outportb(0x40, HIBYTE(TIMER_FREQ));
	OPL3Silence();
	music_playing = false;
}

void ShutdownOPL3(void)
{
	/* reset timer to default 18.2Hz */
	outportb(0x43, 0x36);
	outportb(0x40, 0);
	outportb(0x40, 0);
	setvect(TIMER_VECT, oldHandler);
	OPL3Silence();
	music_playing = false;
}

/* DOSBox Raw OPL format:
 * http://www.shikadi.net/moddingwiki/DRO_Format#Header_2
 */
void LoadSong(const char *filename)
{
	FILE *file;
	unsigned char buf[1024];
	unsigned char reg_table_len;
	unsigned long tmp_ulong;
	unsigned long loaded = 0;
	short n;

	file = fopen(filename, "rb");
	if (!file)
		return;

	fread(buf, 8, 1, file);
	Assert(!strncmp(buf, "DBRAWOPL", 8), "invalid music signature");
	fread(&tmp_ulong, 4, 1, file);
	Assert(tmp_ulong == 2, "invalid music version");

	fread(&tmp_ulong, 4, 1, file); /* size in words */
	song_len = tmp_ulong * 2; /* convert to bytes */

	fread(buf, 7, 1, file); /* ignored fields */

	fread(&cmd_shortdelay, 1, 1, file);
	fread(&cmd_longdelay, 1, 1, file);

	fread(&reg_table_len, 1, 1, file);
	Assert(reg_table_len <= sizeof(reg_table), "register table too large");
	fread(reg_table, 1, reg_table_len, file);

	song_data = farmalloc(song_len);
	Assert(song_data != NULL, "couldn't alloc song_data");
	song_ptr = song_data;
	while (loaded < song_len)
  {
		n = fread(buf, 1, sizeof(buf), file);
		if (n > song_len - loaded)
		{
			n = song_len - loaded;
		}
		Assert(n != 0, "error reading song");
		_fmemcpy(song_ptr, buf, n);
		loaded += n;
		song_ptr += n;
	}
}

void PlayMusic(void)
{
	if (music_playing)
		return;
	music_playing = true;

	/* Reset to beginning of stream and queue music next tick */
	song_ptr = song_data;
	opl3_next = 1;
}

void StopMusic(void)
{
	/* Kill the timer, and silence the chip */
	opl3_next = 0;
	music_playing = false;
	OPL3Silence();
}

static void OPL3Write(unsigned offset, unsigned reg, unsigned val)
{
	int i;
	outportb(OPL3_PORT + offset, reg);
	for (i = 0 ; i < 5 ; i++)
		inportb(OPL3_PORT + offset);
	outportb(OPL3_PORT + offset + 1, val);
	for (i = 0 ; i < 5 ; i++)
		inportb(OPL3_PORT + offset);
}

static void OPL3Frame(void)
{
	while (1)
	{
		/* read one command */
		unsigned char idx = *song_ptr++;
		unsigned char val = *song_ptr++;

		if (song_ptr - song_data > song_len)
			song_ptr = song_data;
		if (idx == cmd_shortdelay)
		{ /* val is delay_ms - 1. */
			opl3_next = val + 1;
			break;
		}
		else if (idx == cmd_longdelay)
		{ /* val is delay_ms / 256 - 1. */
			opl3_next = (val + 1) << 8;
			break;
		}
		else
		{
			/* high bit selects alternate register set */
			/* (port 38a/38b instead of 388/389) */
			/* low 7 bits map to a register via the reg_table */
			OPL3Write((idx >> 7) << 1, reg_table[idx & 0x7f], val);
		}
	}
}

static void OPL3Silence(void)
{
	unsigned char offset;
	unsigned i;

	for (offset = 0 ; offset <= 2 ; offset += 2)
	{
		for (i = 0xb0 ; i <= 0xb8 ; i++)
		{ /* write all instrument KEY-ON to zero */
			OPL3Write(offset, i, 0);
		}
		/* And disable all drum channels */
		OPL3Write(0xbd, i, 0);
	}
}

void MSTimerDelay(unsigned milliseconds) {
	/* Do the same thing as the Borland libc's delay(), except
	 * spin on the ms counter maintained by our interrupt handler,
	 * instead of reading the timer directly as Borland's does.
	 * (We're not changing the tick rate, but Borland's doesn't deal
	 * with the reload value being anything other than 64k.
	 */
	unsigned long stop;
	unsigned cur, prev;
	stop = (prev = ms) + milliseconds;

	while ((cur = ms) < stop)
	{
		if (cur < prev)
		{
			if (stop < 0x10000L)
				break;
			stop -= 0x10000L;
		}
		prev = cur;
	}
}
